﻿using System;
using System.Collections.Generic;
using System.Linq;
using PortableVersion.Core.General;

namespace PortableVersion.Core.FileSystem.Services
{
    public class DiffService : IDiffService<FileItem>
    {
        /// <summary>
        /// Generates the diff between source and target.
        /// Note that we assume meta has unique key as the file's creationDate is used as an artificial key when meta is generated.
        /// For any item from source that exists in meta, unique key is assumed as well.
        /// These assumption may get broken when someone tempers with the created time of files or if the number of files in meta
        /// are extremely high (i.e. billions) which is less likely than alien's taking over the earth.
        /// </summary>
        /// <param name="source"></param>
        /// <param name="target"></param>
        /// <param name="metaData"></param>
        /// <returns></returns>
        public IEnumerable<Diff<FileItem>> GenerateDiff(IVersioningSource<FileItem> source, IVersioningSource<FileItem> target, IMetadataService<FileItem> metaData)
        {
            //Get the event types for source
            var soruceEventTypes = source.Items.GroupJoin(metaData.Items, s => s.Key, m => m.Key,
                                                          (s, ml) => GetSourceDiff(s, ml.FirstOrDefault()))
                                                          .Concat(
                                                            metaData.Items.Where(m => !source.Items.Any(s => s.Key ==  m.Key)).Select(
                                                                m => GetSourceDiff(null, m)
                                                            )
                                                          )
                .ToList();
            var targetEventTypes =
                metaData.Items.GroupJoin(target.Items, m => GetRelativePath(source, m), t => GetRelativePath(target, t),
                                         (m, tl) => GetTargetDiff(m, tl.FirstOrDefault()))
                    .ToList();
            return soruceEventTypes
                .Where(s => s.Meta != null)
                .GroupJoin(targetEventTypes, s => s.Meta.Key, t => t.Meta.Key,
                           (s, tl) => MergeDiff(s, tl.FirstOrDefault()))
                .Concat(soruceEventTypes.Where(s => s.Meta == null));
        }

        private static Diff<FileItem> MergeDiff(Diff<FileItem> source, Diff<FileItem> target)
        {
            return new Diff<FileItem>(source.Source, source.SourceEvent, target.Target, target.TargetEvent, source.Meta);
        }

        private static String GetRelativePath(IVersioningSource<FileItem> source, FileItem file)
        {
            return file.Path.Substring(source.Path.Length + 1);
        }

        private static Diff<FileItem> GetSourceDiff(FileItem source, FileItem meta)
        {
            if (source == null)
                meta.AssertNotNull("source and meta together");
            var ev = DiffEvent.Unchanged;
            if (source == null && meta != null)
                ev = DiffEvent.Deleted;
            else
            if (source != null && meta == null)
                ev = DiffEvent.Added;
// ReSharper disable PossibleNullReferenceException
            else if (source.Path != meta.Path)
// ReSharper restore PossibleNullReferenceException
                ev = DiffEvent.Renamed;
            else
            if (!source.IsFolder && source.ModifiedDate > meta.ModifiedDate)
                    ev = DiffEvent.Modified;
            return new Diff<FileItem>(source, ev, null, DiffEvent.Unchanged, meta);
        }

        private static Diff<FileItem> GetTargetDiff(FileItem meta, FileItem target)
        {
            meta.AssertNotNull("meta");
            var ev = DiffEvent.Unchanged;
            if (target == null)
                ev = DiffEvent.Deleted;
            else if (target.ModifiedDate > meta.ModifiedDate)
                ev = DiffEvent.Modified;
            return new Diff<FileItem>(null, DiffEvent.Unchanged, target, ev, meta);
        }
    }
}
